package jmine.tec.rpc.server.comm;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.rpc.common.RPCException;
import jmine.tec.rpc.common.metadata.RestServiceMetadata;
import jmine.tec.rpc.common.request.RestRequest;
import jmine.tec.rpc.common.request.RestResponse;
import jmine.tec.rpc.common.serialization.Serializer;
import jmine.tec.utils.io.IOUtils;
import jmine.tec.utils.io.http.exception.HttpClientErrorException;
import jmine.tec.utils.io.http.exception.HttpException;
import jmine.tec.web.servlet.rest.AbstractRestSupport;
import jmine.tec.web.servlet.rest.RestService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Stub para serviços REST
 * 
 * @author lundberg
 * @param <S> tipo de serviço implementado
 */
public class RestServiceStub<S> extends AbstractRestSupport implements RestService {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestServiceStub.class);

    private final Serializer serializer;

    private final Class<S> service;

    private final S implementor;

    private final String prefix;

    /**
     * Construtor
     * 
     * @param serializer serializer
     * @param service service
     * @param implementor implementor
     * @param prefix prefix
     */
    public RestServiceStub(Serializer serializer, Class<S> service, S implementor, String prefix) {
        RestServiceMetadata.verifyService(service);
        this.serializer = serializer;
        this.service = service;
        this.implementor = implementor;
        this.prefix = prefix;
    }

    /**
     * {@inheritDoc}
     */
    public void post(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try {
            this.doPost(request, response);
        } catch (HttpException e) {
            LOGGER.error("Unexpected error.", e);
            this.error(e.getHttpResponse(), e.getMessage(), response);
        } catch (SecurityException e) {
            LOGGER.error("Internal server error.", e);
            this.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error.", response);
        } catch (NoSuchMethodException e) {
            LOGGER.error("Bad request.", e);
            this.error(HttpServletResponse.SC_BAD_REQUEST, "Requested method not found.", response);
        } catch (IllegalArgumentException e) {
            LOGGER.error("Bad request.", e);
            this.error(HttpServletResponse.SC_BAD_REQUEST, "Illegal arguments provided.", response);
        } catch (IllegalAccessException e) {
            LOGGER.error("Internal server error.", e);
            this.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error.", response);
        } catch (InvocationTargetException e) {
            LOGGER.error("Internal server error.", e);
            this.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error.", response);
        } catch (RPCException e) {
            LOGGER.error("Internal server error.", e);
            this.error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error.", response);
        } catch (IOException e) {
            LOGGER.error("I/O error.", e);
            throw e;
        } catch (RuntimeException t) {
            LOGGER.error("Unexpected runtime exception.", t);
            throw t;
        } catch (Error e) {
            LOGGER.error("Unexpected error.", e);
            throw e;
        }
    }

    /**
     * @param request request
     * @param response response
     * @throws IOException IOException
     * @throws NoSuchMethodException NoSuchMethodException
     * @throws IllegalAccessException IllegalAccessException
     * @throws InvocationTargetException InvocationTargetException
     * @throws HttpException HttpException
     */
    private void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, NoSuchMethodException,
            IllegalAccessException, InvocationTargetException, HttpException {
        RestRequest serviceRequest = this.deserialize(request);
        String methodName = serviceRequest.getMethod();
        Class[] parameterTypes = serviceRequest.getParameterTypes();
        Object[] parameters = serviceRequest.getParameters();
        Method method = this.service.getMethod(methodName, parameterTypes);
        RestResponse restResponse = this.invoke(method, parameters);
        InputStream responseBody = this.serializer.serialize(restResponse);
        IOUtils.copyFully(responseBody, response.getOutputStream());
    }

    /**
     * Invoca o método e encapsula o resultado
     * 
     * @param parameters parameters
     * @param method method
     * @return RestResponse
     * @throws IllegalAccessException IllegalAccessException
     */
    private RestResponse invoke(Method method, Object[] parameters) throws IllegalAccessException {
        try {
            return new RestResponse(method.invoke(this.implementor, parameters));
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof Exception) {
                LOGGER.debug("Exception thrown on service implementor.", cause);
            } else {
                LOGGER.error("Unchecked exception thrown by implementor.", cause);
            }
            return new RestResponse(e.getCause());
        }
    }

    /**
     * Lê a requisição
     * 
     * @param request request
     * @return ServiceRequest
     * @throws IOException IOException
     * @throws HttpException HttpException
     */
    private RestRequest deserialize(HttpServletRequest request) throws IOException, HttpException {
        try {
            Object serviceRequest = this.serializer.deserialize(request.getInputStream());
            if (!(serviceRequest instanceof RestRequest)) {
                throw new HttpClientErrorException(HttpServletResponse.SC_BAD_REQUEST, "Invalid request input.");
            }
            return (RestRequest) serviceRequest;
        } catch (ParseException e) {
            throw new HttpClientErrorException(HttpServletResponse.SC_BAD_REQUEST, "Invalid request format.", e);
        }

    }

    /**
     * {@inheritDoc}
     */
    public void delete(HttpServletRequest request, HttpServletResponse response) throws IOException {
        this.error(HttpServletResponse.SC_METHOD_NOT_ALLOWED, this.getPreffix(), response);
    }

    /**
     * {@inheritDoc}
     */
    public void get(HttpServletRequest request, HttpServletResponse response) throws IOException {
        this.error(HttpServletResponse.SC_METHOD_NOT_ALLOWED, this.getPreffix(), response);
    }

    /**
     * {@inheritDoc}
     */
    public void put(HttpServletRequest request, HttpServletResponse response) throws IOException {
        this.error(HttpServletResponse.SC_METHOD_NOT_ALLOWED, this.getPreffix(), response);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPreffix() {
        return this.prefix;
    }
}
